export class ApiError extends Error {
  status: number;
  body?: unknown;
  constructor(message: string, status: number, body?: unknown) {
    super(message);
    this.status = status;
    this.body = body;
  }
}

const baseUrl = (
  process.env.NEXT_PUBLIC_API_BASE ||
  process.env.NEXT_PUBLIC_API_URL ||
  'http://localhost:3001'
).replace(/\/$/, '');
const DEFAULT_TIMEOUT_MS = 15000;

function withTimeout(signal?: AbortSignal | null, timeoutMs: number = DEFAULT_TIMEOUT_MS) {
  const controller = new AbortController();
  const id = setTimeout(() => controller.abort(new Error('Request timeout')), timeoutMs);
  function cleanup() { clearTimeout(id); }
  const composite = mergeSignals(signal, controller.signal, cleanup);
  return { signal: composite, cleanup };
}

function mergeSignals(a?: AbortSignal | null, b?: AbortSignal | null, onAbort?: () => void): AbortSignal | undefined {
  if (!a && !b) return undefined;
  const controller = new AbortController();
  const onA = () => controller.abort(a?.reason);
  const onB = () => controller.abort(b?.reason);
  a?.addEventListener('abort', onA);
  b?.addEventListener('abort', onB);
  controller.signal.addEventListener('abort', () => {
    a?.removeEventListener('abort', onA);
    b?.removeEventListener('abort', onB);
    onAbort?.();
  });
  return controller.signal;
}

export async function apiPost<T>(path: string, body: unknown, init?: RequestInit & { timeoutMs?: number }): Promise<T> {
  const { signal, cleanup } = withTimeout(init?.signal, init?.timeoutMs);
  const authHeaders = (withAuth(init)?.headers || {}) as Record<string, string>;
  const mergedHeaders = {
    'Content-Type': 'application/json',
    ...(init?.headers || {}),
    ...authHeaders,
  } as Record<string, string>;

  const { headers: _omitHeaders, ...restInit } = init || {};

  const res = await fetch(`${baseUrl}${path}`, {
    method: 'POST',
    headers: mergedHeaders,
    body: JSON.stringify(body),
    ...restInit,
    signal,
  });
  cleanup?.();
  const text = await res.text();
  const json = safeJson(text);
  if (!res.ok) throw new ApiError(getErrorMessage(json, text), res.status, json ?? text);
  return json as T;
}

export async function apiGet<T>(path: string, init?: RequestInit & { timeoutMs?: number }): Promise<T> {
  const { signal, cleanup } = withTimeout(init?.signal, init?.timeoutMs);
  const authHeaders = (withAuth(init)?.headers || {}) as Record<string, string>;
  const mergedHeaders = {
    ...(init?.headers || {}),
    ...authHeaders,
  } as Record<string, string>;

  const { headers: _omitHeaders, ...restInit } = init || {};

  const res = await fetch(`${baseUrl}${path}`, {
    method: 'GET',
    headers: mergedHeaders,
    ...restInit,
    signal,
  });
  cleanup?.();
  const text = await res.text();
  const json = safeJson(text);
  if (!res.ok) throw new ApiError(getErrorMessage(json, text), res.status, json ?? text);
  return json as T;
}

export async function apiPut<T>(path: string, body: unknown, init?: RequestInit & { timeoutMs?: number }): Promise<T> {
  const { signal, cleanup } = withTimeout(init?.signal, init?.timeoutMs);
  const authHeaders = (withAuth(init)?.headers || {}) as Record<string, string>;
  const mergedHeaders = {
    'Content-Type': 'application/json',
    ...(init?.headers || {}),
    ...authHeaders,
  } as Record<string, string>;

  const { headers: _omitHeaders, ...restInit } = init || {};

  const res = await fetch(`${baseUrl}${path}`, {
    method: 'PUT',
    headers: mergedHeaders,
    body: JSON.stringify(body),
    ...restInit,
    signal,
  });
  cleanup?.();
  const text = await res.text();
  const json = safeJson(text);
  if (!res.ok) throw new ApiError(getErrorMessage(json, text), res.status, json ?? text);
  return json as T;
}

export async function apiDelete<T>(path: string, init?: RequestInit & { timeoutMs?: number }): Promise<T> {
  const { signal, cleanup } = withTimeout(init?.signal, init?.timeoutMs);
  const authHeaders = (withAuth(init)?.headers || {}) as Record<string, string>;
  const mergedHeaders = {
    ...(init?.headers || {}),
    ...authHeaders,
  } as Record<string, string>;

  const { headers: _omitHeaders, ...restInit } = init || {};

  const res = await fetch(`${baseUrl}${path}`, {
    method: 'DELETE',
    headers: mergedHeaders,
    ...restInit,
    signal,
  });
  cleanup?.();
  const text = await res.text();
  const json = safeJson(text);
  if (!res.ok) throw new ApiError(getErrorMessage(json, text), res.status, json ?? text);
  return json as T;
}

function safeJson(text: string) {
  try { return text ? JSON.parse(text) : undefined; } catch { return undefined; }
}

function getErrorMessage(json: any, fallback: string) {
  if (json && typeof json === 'object') {
    const msg = Array.isArray(json.message)
      ? json.message.join('\n')
      : (json.message || json.error || json.msg);
    return msg || fallback || 'Request failed';
  }
  return fallback || 'Request failed';
}

export function withAuth(init?: RequestInit): RequestInit {
  const token = typeof window !== 'undefined' ? localStorage.getItem('accessToken') : null;
  return {
    ...init,
    headers: {
      ...(init?.headers || {}),
      ...(token ? { Authorization: `Bearer ${token}` } : {}),
    },
  };
}